Scaffold Swift-implemented drop-in replacement for libbluetooth.so.3 - #63
Open
colemancda wants to merge 73 commits into
Open
Scaffold Swift-implemented drop-in replacement for libbluetooth.so.3#63colemancda wants to merge 73 commits into
colemancda wants to merge 73 commits into
Conversation
Drops the 9 stubs now covered by BluetoothLinuxABI's device management family (96 remaining, 132 implemented).
hciCommand/hciStatus/hciRequest capture the send/check-status/copy-out shapes that every hci_send_req-based command wrapper in lib/hci.c repeats, so each wrapper is a short, direct transcription of its reference instead of duplicating the boilerplate.
hci_create_connection, hci_disconnect, hci_authenticate_link, hci_encrypt_link, hci_change_link_key, hci_switch_role, hci_park_mode, and hci_exit_park_mode.
hci_read_remote_name(_with_clock_offset), hci_read_remote_name_cancel, hci_read_remote_version, hci_read_remote_features, hci_read_remote_ext_features, and hci_read_clock_offset.
hci_read_local_version, hci_read_local_commands, hci_read_local_features, hci_read_local_ext_features, hci_read_bd_addr, hci_read_local_name, and hci_write_local_name.
White list and resolving list management, scan and advertising control, and LE connection establishment/update/remote-features query.
Class of device, voice setting, inquiry access codes, stored link keys, inquiry/AFH/inquiry-mode toggles, extended inquiry response, simple pairing, OOB data, transmit power, link policy/supervision timeout, AFH classification, and per-connection link quality/RSSI/AFH map/clock queries — 33 symbols.
Unlike the rest of the command wrapper family, this goes through a single ioctl(HCIINQUIRY) rather than hci_send_req — the request header and resulting inquiry_info records share one kernel buffer, the same shape as HCIGETDEVLIST in HCIDevice.swift.
Drops the 71 stubs now covered by the HCI command wrapper family (25 remaining — the SDP session functions — 203 implemented).
PureSwift/Bluetooth 8.1.0 folds the 75 pure sdp_* symbols into BluetoothABI rather than shipping them as a separate static library, so there is no BluetoothSDP CMake target to name here any more. Its symbols come in with BluetoothABI, which is already linked whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Encoding a search pattern or an attribute ID list as a data element sequence, and appending a continuation state. The reference keeps the equivalents private to sdp.c and exports none of them, so they are reimplemented here rather than linked against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
sdp_create, sdp_connect, sdp_close, sdp_get_socket, sdp_get_error, sdp_gen_tid, sdp_set_notify and sdp_general_inquiry. sdp_session_t is a non-opaque struct that some callers read directly, so its fields are kept populated with real values rather than treated as opaque. The private per-session state (the reference's struct sdp_transaction) is not part of any vendored header and is reimplemented as SDPTransaction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
sdp_service_search_req, sdp_service_attr_req and sdp_service_search_attr_req, built on the sdp_send_req_w4_rsp send/wait-for-response primitive, following continuation state until the response is complete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Registers the twelve socket-bound SDP client session functions so gen_stubs.py stops emitting stubs for them, and retargets the pure sdp_* note at PureSwift/Bluetooth's Sources/BluetoothABI, which is where they live as of 8.1.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
13 stubs remaining, 215 implemented of 218 exported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The SDP session sources import BluetoothABI for the PDU codec, which the SwiftPM manifest already provides but the CMake build did not, so libBluetoothLinuxABI.a failed to compile with "no such module". The link has to be declared after add_subdirectory, which is where the target is defined. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Every multi-byte field in an SDP PDU sits wherever the preceding fields leave it — the 5-byte header alone puts the body on an odd address — so writing one through a typed pointer is not valid even where x86 tolerates it. The registration and asynchronous request builders use this instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
sdp_record_register/unregister/update, their sdp_device_record_* forms, and the two _binary entry points underneath them. The PDUs these exchange (0x75 through 0x80) are explicitly outside the Bluetooth specification — the vendored sdp.h says so where it defines them — so the wire layouts were recovered from the reference libbluetooth.so.3, which is the only thing that documents them. Two of its behaviours are reproduced rather than corrected because a client can observe them: the response status is loaded in host order rather than with ntohs, and sdp_device_record_update_binary is an exported stub that returns -1 without sending anything. Bounds that the reference delegates to _FORTIFY_SOURCE (where an oversized record aborts the process) are checked here and reported as EINVAL instead, which cannot break a caller that already worked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
sdp_service_search_async, sdp_service_attr_async and sdp_service_search_attr_async only build and send; sdp_process does everything about the reply — reassembling a response split across continuation states, resending the request for each next fragment, and finally invoking the sdp_set_notify callback. sdp_process's return value is easy to misread: 0 means "not finished, call me again when the socket is readable" and -1 means the transaction is over and the callback has run. -1 is the normal ending and says nothing about success. The record-count accumulation in a fragmented search response adds two big-endian values without swapping either, so a count that carries out of the low byte comes out wrong. That is what the reference writes and what a client reads back, so it is reproduced with a comment rather than fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Registers the nine record registration functions and the four asynchronous ones, which leaves nothing stubbed: 228 implemented, 218 of them exported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Empty: every exported symbol now has a real implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The container was pinned to swift:6.0.2, which can no longer resolve the package at all: PureSwift/Bluetooth 8.1.0 declares swift-tools-version:6.2, and 6.0.2 rejects it with "incompatible tools version" before compiling anything. The build and test steps now run as a matrix over 6.2.3 and 6.3.3, debug and release. Adds a job that builds and tests with SWIFTPM_BLUETOOTH_CABI=1 and then runs the differential conformance drivers against the system libbluetooth.so.3. Nothing in CI compiled a line of the C ABI before this, so all 228 exported symbols were unguarded. That job is 6.3-only because the boundary is bound with `@c`, which 6.2 rejects. Coverage moves to its own job so it runs once rather than once per matrix cell, which also keeps the uploaded artifact names unambiguous. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
SwiftPM builds the same sources but not the shipped artifact: soname, symbol versioning and the export list are only expressible in CMakeLists.txt, so nothing in CI was checking the thing that actually gets installed. This builds it, runs check-exports — the assertion that the library exports exactly the reference's symbol set, which is what would catch the drop-in premise breaking — and installs it. Needs a PureSwift/Bluetooth checkout, since the shared object links the non-socket half of the C ABI out of that package's own CMakeLists. It is pinned to the version Package.swift depends on so the two halves cannot drift apart here. noble rather than jammy: CMakeLists.txt requires CMake 3.26+ and jammy's apt carries only 3.22. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
testClassicAddressDecoding expected a wire address type of 0 to decode back to nil, but 0 is not an "unspecified" sentinel: it is the BR/EDR address type, which is exactly why the encoder writes nil as 0. The round trip is asymmetric by construction — nil encodes to 0, and 0 decodes to .bredr — so .bredr is the correct expectation, and the test has been failing since it was added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The swift:6.3.3-noble container does not ship a python3, and configure fails at find_package(Python3 REQUIRED) — the stub table and version script are generated by scripts/*.py at build time. This never surfaced locally because the host interpreter satisfied the search. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The differential driver compares our output against whatever libbluetooth3 the distro ships, and our tables match current BlueZ (5.79, the Debian 13 reference this port is verified against). Jammy's 5.64 predates both bus type 11 (IPC) and HCI/LMP version 0x0d (5.4), and noble's 5.72 still lacks the former, so CI prints the fallback where we print the newer name. Record those eight lines in known-differences.txt, the file compare.sh already subtracts from the diff. Both sides of each pair are listed because the subtraction is direction-blind; on a reference that knows these values the outputs are identical and no entry applies. Verified against jammy's actual .so (extracted from ubuntu:22.04) via BT_REFERENCE_LIB + LD_LIBRARY_PATH: all 8 differing lines accepted, and against the local 5.79 the output remains byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The header calls memset without declaring it, so clang implicitly declares the builtin — `void *(void *, int, unsigned long)` — and that declaration becomes part of the CBluetoothLinuxTest module, where it shadows SwiftGlibc's annotated memset. Debug builds get away with it, but in release, cross-module optimization deserializes FoundationEssentials SIL that references SwiftGlibc.memset, resolution finds only the mismatched builtin, and swift-frontend aborts with a deserialization failure (signal 6) while compiling the test target. This is what has been failing every release-configuration CI cell. Verified: swift test -c release now builds and passes (60 tests, 0 failures) on 6.3.3, where it previously crashed the compiler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
In a container job, ${{ github.workspace }} expands to the host runner
path (/home/runner/work/...), but the step executes inside the
container, where the workspace is mounted at /__w/... — so configure
looked for the Bluetooth checkout at a path that does not exist and
failed. The GITHUB_WORKSPACE environment variable is set by the runner
inside the container and resolves to the mounted path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Scaffolding for a drop-in, Swift-implemented replacement for BlueZ's shared
libbluetooth.so.3. The deliverable is a library that loads, resolves all 218 symbols the reference exports, and fails loudly (by symbol name) on any call not implemented yet — so the rest of the port can proceed incrementally, one symbol family at a time.Nothing in the
bluezpackage actually links this shared library —bluetoothd,bluetoothctl,btmon,hciconfigandsdptoolall statically linklibbluetooth-internal.a— so replacing it can't break the Bluetooth stack itself; it exists solely for third-party consumers.Sources/CBluetoothLinuxABIvendors the eleven public BlueZ headers verbatim (GPL-2.0-or-later, isolated in its own directory with its own LICENSE and README) and generates a stub for every symbol not yet implemented, viascripts/gen_stubs.py. Each stub's signature is read out of the vendored headers, so it's checked against the real declaration rather than hand-typed.scripts/symbols.txtrecords the reference export surface (218, split 17/101/100 acrossbluetooth.c/hci.c/sdp.c);scripts/implemented.txtrecords the 203 currently implemented: thebluetooth.c+bt_uuid_*families and the SDP data/list/record/codec layer (both implemented in PureSwift/Bluetooth), plus this repo's own HCI string converter family, HCI device management family, and the 71-symbol HCI command wrapper family (hci_inquiry,hci_create_connection,hci_read_*,hci_write_*,hci_le_*, etc.) — everything BlueZ'slib/hci.cexports excepthci_strtoptype-adjacent string helpers already covered above.scripts/gen_symbols.pyderives the linker version script and the flat export list from these;scripts/check-exports.shasserts the built library against it.scripts/ownership.mdrecords the return/who-frees/lifetime contract for each symbol, ahead of implementation — the conventions aren't uniform (some allocate on the heap, some write into a caller buffer, some return static pointers) and guessing produces leaks or double frees no type checker would catch.Sources/BluetoothLinuxABIimplements the 21-symbol HCI string converter family, using generated lookup tables (scripts/generate-hci-tables.py, parsed directly from BlueZ'shci.c) for the bus/device-flag/packet-type/link-mode/version/command-name mappings; the 9-symbol HCI device management family (rawAF_BLUETOOTH/BTPROTO_HCIsocket open/close, device info/address/id lookup, enumeration, routing, and sending commands/requests); and the 71-symbol HCI command wrapper family (connection/link-policy management, remote-device queries, local controller info, controller configuration, LE commands, and inquiry) built onhciCommand/hciStatus/hciRequesthelpers that capture the send/check-status/copy-out shape every wrapper inlib/hci.crepeats. All of it talks to the kernel directly via socket/bind/ioctl/writev/poll rather than through BluetoothLinux's own async HostController/Socket infrastructure, since the ABI surface has to be synchronous and match the reference's wire layout precisely.CMakeLists.txtbuildslibbluetooth.so.3.19.15by linking aPureSwift/Bluetoothcheckout's static archives (-DBLUETOOTH_PACKAGE_PATH) together withBluetoothLinuxABIand the generated stubs, with the soname, version, and a pinned export list that Package.swift can't express.Conformance/compare.shandConformance/conformance_hci_strings.cdifferentially test the HCI string converter family against the systemlibbluetooth.so.3; the phase-1 and SDP drivers stay in thePureSwift/Bluetoothcheckout alongside the symbols they cover.Verified
check-exportstarget).libbluetooth.so.3, aside from a handful of already-documented deltas (bt_compidtostrnaming, onememcmp-magnitude case) recorded in each repo'sknown-differences.txt.hci_open_dev) aborts withlibbluetooth (PureSwift): hci_open_dev is not implemented yet.rather than crashing unhelpfully or corrupting state.Test plan
swift build— default configuration, unaffectedSWIFTPM_BLUETOOTH_CABI=1 swift build— C ABI target buildsSWIFTPM_BLUETOOTH_CABI=1 swift test— passes, including the newBluetoothLinuxABITestscmake -B build -G Ninja -DBLUETOOTH_PACKAGE_PATH=<Bluetooth checkout> && cmake --build build—libbluetooth.so.3.19.15buildscmake --build build --target check-exports— 218/218 symbols matchConformance/compare.sh— identical output against the reference library for the HCI string converter driverKnown gap
hci_send_cmd/hci_send_reqand everything built on them — the entire 71-symbol HCI command wrapper family, plushci_inquiry'sioctl(HCIINQUIRY)call — have not been exercised against a real or virtual HCI device. Only their non-socket paths (input validation,hci_send_cmd's wire-format framing over a plain pipe) are unit tested. No/dev/vhciaccess was available (root-only in this environment) to build a differential driver for this family; each wrapper is a careful, field-by-field transcription of itslib/hci.creference, cross-checked against the vendored struct layouts, but unverified end-to-end.